package com.github.aesteve.vertx.nubes.context;

import com.github.aesteve.vertx.nubes.handlers.impl.DefaultErrorHandler;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;

import java.util.ArrayList;
import java.util.List;

import static java.util.Collections.emptyList;

/**
 * Reflects the pagination state for a RoutingContext (request parameters)
 * Provides service methods to :
 * - generate pagination Link headers
 * - generate paginationContext to render a page on the server
 *
 * @author aesteve
 */
public class PaginationContext {

  public static final String DATA_ATTR = "paginationContext";

  private static final String CURRENT_PAGE_QUERY_PARAM = "page";
  private static final String PER_PAGE_QUERY_PARAM = "perPage";
  private static final Integer DEFAULT_PER_PAGE = 30;
  private static final Integer MAX_PER_PAGE = 100;

  private Integer pageAsked = 1;
  private Integer itemsPerPage = DEFAULT_PER_PAGE;
  private Integer totalPages; // Will be set once the request has been processed (payload)

  /**
   * Prefer using fromRoutingContext but can be instanciated directly
   * (for static page generation for instance)
   *
   */
  private PaginationContext(Integer pageAsked, Integer itemsPerPage) {
    if (pageAsked != null)
      this.pageAsked = pageAsked;
    if (itemsPerPage != null)
      this.itemsPerPage = itemsPerPage;
  }

  /**
   * The preferred way to create a PaginationContext
   *
   */
  public static PaginationContext fromContext(RoutingContext context) {
    HttpServerRequest request = context.request();
    String pageStr = request.getParam(PaginationContext.CURRENT_PAGE_QUERY_PARAM);
    String perPageStr = request.getParam(PaginationContext.PER_PAGE_QUERY_PARAM);
    Integer page = null;
    Integer perPage = null;
    try {
      if (pageStr != null) {
        page = Integer.parseInt(pageStr);
      }
      if (perPageStr != null) {
        perPage = Integer.parseInt(perPageStr);
      }
    } catch (NumberFormatException e) {
      DefaultErrorHandler.badRequest(context, "Invalid pagination parameters : expecting integers");
    }
    if (perPage != null && perPage > PaginationContext.MAX_PER_PAGE) {
      DefaultErrorHandler.badRequest(context, "Invalid " + PaginationContext.PER_PAGE_QUERY_PARAM + " parameter, max is " + PaginationContext.MAX_PER_PAGE);
    }
    return new PaginationContext(page, perPage);
  }

  public String buildLinkHeader(HttpServerRequest request) {
    List<String> links = getNavLinks(request);
    if (links.isEmpty()) {
      return null;
    }
    return String.join(", ", links);
  }

  private List<String> getNavLinks(HttpServerRequest request) {
    if (totalPages == null) {
      return emptyList();
    }
    List<String> links = new ArrayList<>();
    if (pageAsked > 1) {
      links.add(pageUrl(request, 1, "first"));
      links.add(pageUrl(request, pageAsked - 1, "prev"));
    }
    if (pageAsked < totalPages) {
      links.add(pageUrl(request, totalPages, "last"));
      links.add(pageUrl(request, pageAsked + 1, "next"));
    }
    return links;
  }

  private String pageUrl(HttpServerRequest request, int pageNum, String rel) {
    StringBuilder sb = new StringBuilder("<");
    String url = request.absoluteURI();
    if (!url.contains("?")) { // can't rely on params() 'cause we might have injected some stuff (routing)
      url += "?" + CURRENT_PAGE_QUERY_PARAM + "=" + pageNum;
      url += "&" + PER_PAGE_QUERY_PARAM + "=" + itemsPerPage;
    } else {
      if (url.indexOf(CURRENT_PAGE_QUERY_PARAM + "=") > url.indexOf('?')) {
        url = url.replaceAll(CURRENT_PAGE_QUERY_PARAM + "=([^&]+)", CURRENT_PAGE_QUERY_PARAM + "=" + pageNum);
      } else {
        url += "&" + CURRENT_PAGE_QUERY_PARAM + "=" + pageNum;
      }
      if (!url.contains("&" + PER_PAGE_QUERY_PARAM) && !url.contains("?" + PER_PAGE_QUERY_PARAM)) {
        url += "&" + PER_PAGE_QUERY_PARAM + "=" + itemsPerPage;
      }
    }
    sb.append(url);
    sb.append(">; ");
    sb.append("rel=\"").append(rel).append("\"");
    return sb.toString();
  }

  public Integer getItemsPerPage() {
    return itemsPerPage;
  }

  public Integer getTotalPages() {
    return totalPages;
  }

  public void setNbItems(Integer nbTotalItems) {
    this.totalPages = nbTotalItems / itemsPerPage;
    if (totalPages == 0) {
      totalPages = 1;
    }
    int modulo = nbTotalItems % itemsPerPage;
    if (modulo > 0) {
      this.totalPages = this.totalPages + 1;
    }
  }

  public void setNbItems(Long nbTotalItems) {
    setNbItems(nbTotalItems.intValue());
  }

  public boolean hasMorePages() {
    return totalPages > pageAsked;
  }

  public int firstItemInPage() {
    return itemsPerPage * (pageAsked - 1);
  }

  public int lastItemInPage() {
    return firstItemInPage() + itemsPerPage;
  }

  /**
   * Creates a JsonObject containing all pagination
   * that can be injected and evaluated
   * when rendering a page on the server
   *
   * @return a JsonObject containing pagination informations
   */
  public JsonObject toJsonObject() {
    JsonObject json = new JsonObject();
    json.put("current", pageAsked);
    json.put("next", pageAsked.equals(totalPages) ? null : pageAsked + 1);
    json.put("last", pageAsked.equals(totalPages) ? null : totalPages);
    json.put("prev", pageAsked.equals(1) ? null : pageAsked - 1);
    json.put("first", pageAsked == 1 ? null : 1);
    json.put("perPage", itemsPerPage);
    json.put("total", totalPages);
    return json;
  }
}
